﻿//-----------------------------------------------------------------------
// <copyright file="RuleSetDictionary.cs" company="tuliprules.org">
//     Copyright (c) 2010 tuplelister, tuliprules.org.
// </copyright>
// <summary>This is the RuleSetDictionary class.</summary>
//-----------------------------------------------------------------------

namespace Tulip.Rules
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Reflection;
    using System.Xml;
    using System.Xml.Schema;
    using System.Xml.Serialization;

    /// <summary>
    /// A collection of keyed rule sets.
    /// </summary>
    [Serializable]
    [XmlSchemaProvider("GetDictionarySchema")]
    [XmlRoot("RuleSetDictionary", Namespace = "http://tuliprules/rulesets/2010/12", IsNullable = true)]
    public class RuleSetDictionary : IDictionary<string, RuleSet>, IXmlSerializable
    {
        /// <summary>
        /// Internal dictionary.
        /// </summary>
        private readonly Dictionary<string, RuleSet> ruleSetDictionary = new Dictionary<string, RuleSet>();

        /// <summary>
        /// Initializes a new instance of the RuleSetDictionary class that is empty, has the 
        /// default initial capacity, and uses the default equality comparer for the key type.
        /// </summary>
        public RuleSetDictionary()
        {
            this.ruleSetDictionary = new Dictionary<string, RuleSet>();
        }

        /// <summary>
        /// Initializes a new instance of the RuleSetDictionary class that contains elements 
        /// copied from the specified rule set dictionary and uses the default equality comparer 
        /// for the key type.
        /// </summary>
        /// <param name="dictionary">
        /// The rule set dictionary whose elements are copied to the new RuleSetDictionary.
        /// </param>
        public RuleSetDictionary(IDictionary<string, RuleSet> dictionary)
        {
            this.ruleSetDictionary = new Dictionary<string, RuleSet>(dictionary);
        }

        /// <summary>
        /// Initializes a new instance of the RuleSetDictionary class that contains elements 
        /// copied from the specified  rule set dictionary and uses the specified 
        /// IEqualityComparer&lt;T&gt;.
        /// </summary>
        /// <param name="dictionary">
        /// The rule set dictionary whose elements are copied to the new RuleSetDictionary.
        /// </param>
        /// <param name="comparer">
        /// The IEqualityComparer&lt;T&gt; implementation to use when comparing keys, or null to use the 
        /// default EqualityComparer&lt;T&gt; for the type of the key.
        /// </param>
        public RuleSetDictionary(IDictionary<string, RuleSet> dictionary, IEqualityComparer<string> comparer)
        {
            this.ruleSetDictionary = new Dictionary<string, RuleSet>(dictionary, comparer);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RuleSetDictionary"/> class. 
        /// The object is empty, has the default initial capacity, and uses the specified 
        /// IEqualityComparer&lt;T&gt;.
        /// </summary>
        /// <param name="comparer">
        /// The IEqualityComparer&lt;T&gt; implementation to use when comparing keys, or null to use the 
        /// default EqualityComparer&lt;T&gt; for the type of the key.
        /// </param>
        public RuleSetDictionary(IEqualityComparer<string> comparer)
        {
            this.ruleSetDictionary = new Dictionary<string, RuleSet>(comparer);
        }

        /// <summary>
        /// Initializes a new instance of the RuleSetDictionary class that is empty, has the 
        /// specified initial capacity, and uses the default equality comparer for the key type.
        /// </summary>
        /// <param name="capacity">
        /// The initial number of elements that the RuleSetDictionary can contain.
        /// </param>
        public RuleSetDictionary(int capacity)
        {
            this.ruleSetDictionary = new Dictionary<string, RuleSet>(capacity);
        }

        /// <summary>
        /// Initializes a new instance of the RuleSetDictionary class that is empty, has the 
        /// specified initial capacity, and uses the specified IEqualityComparer&lt;T&gt;.
        /// </summary>
        /// <param name="capacity">
        /// The initial number of elements that the RuleSetDictionary can contain.
        /// </param>
        /// <param name="comparer">
        /// The IEqualityComparer&lt;T&gt; implementation to use when comparing keys, or null to use the 
        /// default EqualityComparer&lt;T&gt; for the type of the key.
        /// </param>
        public RuleSetDictionary(int capacity, IEqualityComparer<string> comparer)
        {
            this.ruleSetDictionary = new Dictionary<string, RuleSet>(capacity, comparer);
        }

        /// <summary>
        /// Gets an ICollection&lt;T&gt; containing the keys of the dictionary.
        /// </summary>
        /// <value>
        /// An ICollection&lt;T&gt; containing the keys of the dictionary.
        /// </value>
        public ICollection<string> Keys
        {
            get
            {
                return this.ruleSetDictionary.Keys;
            }
        }

        /// <summary>
        /// Gets an ICollection&lt;T&gt; containing the values in the dictionary.
        /// </summary>
        /// <value>
        /// An ICollection&lt;T&gt; containing the values in the dictionary.
        /// </value>
        public ICollection<RuleSet> Values
        {
            get
            {
                return this.ruleSetDictionary.Values;
            }
        }

        /// <summary>
        /// Gets the number of rule sets contained in the dictionary.
        /// </summary>
        /// <value>
        /// The number of rule sets contained in the dictionary.
        /// </value>
        public int Count
        {
            get
            {
                return this.ruleSetDictionary.Count;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the dictionary is read-only.
        /// </summary>
        /// <value>
        /// True if the dictionary is read-only; otherwise, false.
        /// </value>
        public bool IsReadOnly
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Gets or sets the rule set with the specified key.
        /// </summary>
        /// <param name="key">
        /// The key of the element to get or set.
        /// </param>
        /// <returns>
        /// The element with the specified key.
        /// </returns>
        public RuleSet this[string key]
        {
            get
            {
                return this.ruleSetDictionary[key];
            }

            set
            {
                this.ruleSetDictionary[key] = value;
            }
        }

        /// <summary>
        /// Returns an XSD schema for the serialisable facts dictionary.  This is referenced by the XmlSchemaProvider
        /// attribute on this class in order control the XML format. 
        /// </summary>
        /// <param name="xmlSchemaSet">A cache of XSD schemas.</param>
        /// <returns>The qualified XML name of of the FactsDictionary type.</returns>
        public static XmlQualifiedName GetDictionarySchema(XmlSchemaSet xmlSchemaSet)
        {
            if (xmlSchemaSet == null)
            {
                throw new ArgumentNullException("xmlSchemaSet", Properties.Resources.ExValueNotNull);
            }

            try
            {
                var assembly = Assembly.GetExecutingAssembly();
                using (var stream = assembly.GetManifestResourceStream(Properties.Resources.RuleSetDictionarySchema))
                {
                    if (stream == null)
                    {
                        throw new RulesException();
                    }

                    var xsdReader = new XmlTextReader(stream);
                    var xs = XmlSchema.Read(xsdReader, null);
                    xmlSchemaSet.XmlResolver = new XmlUrlResolver();
                    xmlSchemaSet.Add(xs);
                    return new XmlQualifiedName("RuleSetDictionaryType", Properties.Resources.Namespace);
                }
            }
            catch (Exception ex)
            {
                throw new RulesException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        Properties.Resources.ExSchemaNotRetrieved,
                        "RuleSetDictionaryType"),
                    ex);
            }
        }

        /// <summary>
        /// A collection of rule sets, keyed on string identifiers.
        /// </summary>
        /// <param name="key">
        /// The key value.  This will generally be the name of the rule set, and 
        /// may include a version specifier.
        /// </param>
        /// <param name="ruleSet">
        /// A rule set that will be added to the dictionary.
        /// </param>
        public void Add(string key, RuleSet ruleSet)
        {
            this.ruleSetDictionary.Add(key, ruleSet);
        }

        /// <summary>
        /// Determines whether the dictionary contains a rule set with the specified key.
        /// </summary>
        /// <param name="key">A key value.</param>
        /// <returns>
        /// True if the dictionary contains a rule set with the key; otherwise, false.
        /// </returns>
        public bool ContainsKey(string key)
        {
            return this.ruleSetDictionary.ContainsKey(key);
        }

        /// <summary>
        /// Removes the rule set with the specified key from the dictionary.
        /// </summary>
        /// <param name="key">
        /// The key of the rule set to remove.
        /// </param>
        /// <returns>
        /// True if the element is successfully removed; otherwise, false.
        /// </returns>
        public bool Remove(string key)
        {
            return this.ruleSetDictionary.Remove(key);
        }

        /// <summary>
        /// Gets the rule set associated with the specified key.
        /// </summary>
        /// <param name="key">The key whose value to get.</param>
        /// <param name="value">
        /// When this method returns, the rule set associated with the specified key, 
        /// if the key is found; otherwise, null. This parameter is passed uninitialized.
        /// </param>
        /// <returns>
        /// True if the dictionary contains a rule set with the specified key; otherwise, false.
        /// </returns>
        public bool TryGetValue(string key, out RuleSet value)
        {
            return this.ruleSetDictionary.TryGetValue(key, out value);
        }

        /// <summary>
        /// Adds a key-value pair to the dictionary.
        /// </summary>
        /// <param name="item">
        /// A key-value pair for a given rule set.
        /// </param>
        public void Add(KeyValuePair<string, RuleSet> item)
        {
            this.ruleSetDictionary.Add(item.Key, item.Value);
        }

        /// <summary>
        /// Removes all items from the dictionary.
        /// </summary>
        public void Clear()
        {
            this.ruleSetDictionary.Clear();
        }

        /// <summary>
        /// Determines whether the dictionary contains a specific key-value pair.
        /// </summary>
        /// <param name="item">
        /// The key-value pair to locate in the dictionary.
        /// </param>
        /// <returns>True, if the dictioanry contains the specified key-vlaue pair; otherwise false.</returns>
        public bool Contains(KeyValuePair<string, RuleSet> item)
        {
            return this.ruleSetDictionary.ContainsKey(item.Key) && this.ruleSetDictionary[item.Key].Equals(item.Value);
        }

        /// <summary>
        /// Copies the elements of the dictionary to an Array, starting at a particular Array index.
        /// </summary>
        /// <param name="array">
        /// The one-dimensional Array that is the destination of the rule sets copied from the
        /// dictionary. The Array must have zero-based indexing.
        /// </param>
        /// <param name="arrayIndex">
        /// The zero-based index in array at which copying begins.
        /// </param>
        public void CopyTo(KeyValuePair<string, RuleSet>[] array, int arrayIndex)
        {
            var idx = arrayIndex;

            foreach (var ruleSet in this.ruleSetDictionary)
            {
                if (idx < array.Length)
                {
                    array[idx++] = ruleSet;
                }
                else
                {
                    break;
                }
            }
        }

        /// <summary>
        /// Removes the first occurrence of a specific rule set from the dictionary.
        /// </summary>
        /// <param name="item">
        /// The rule set to remove from the dictionary.
        /// </param>
        /// <returns>True, if the specified rule set is found in the dictionary; otherwise false.</returns>
        public bool Remove(KeyValuePair<string, RuleSet> item)
        {
            return this.ruleSetDictionary.Remove(item.Key);
        }

        /// <summary>
        /// Returns an enumerator that iterates through the dictionary.
        /// </summary>
        /// <returns>
        /// An IEnumerator&lt;T&gt; that can be used to iterate through the rule set 
        /// dictionary.
        /// </returns>
        public IEnumerator<KeyValuePair<string, RuleSet>> GetEnumerator()
        {
            return this.ruleSetDictionary.GetEnumerator();
        }

        /// <summary>
        /// Returns an enumerator that iterates through the dictionary.
        /// </summary>
        /// <returns>
        /// An IEnumerator object that can be used to iterate through the collection.
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.ruleSetDictionary.GetEnumerator();
        }

        /// <summary>
        /// Deserialises the XML representation of the dictionary.
        /// </summary>
        /// <param name="reader">An XML Reader used for deserialistion.</param>
        public void ReadXml(XmlReader reader)
        {
            if (reader == null)
            {
                throw new ArgumentNullException("reader", Properties.Resources.ExValueNotNull);
            }

            var wasEmpty = reader.IsEmptyElement;
            reader.Read();

            if (wasEmpty)
            {
                return;
            }

            while (true)
            {
                try
                {
                    var key = this.ReadKey(reader);
                    var value = this.ReadValue(reader);
                    this.Add(key, value);
                }
                catch
                {
                    break;
                }
            }
        }

        /// <summary>
        /// Serialises the dictionary as XML.
        /// </summary>
        /// <param name="writer">An XML writer used for serialisation.</param>
        /// <remarks>
        /// This code is generic.  Override ReadKey, ReadValue, WriteKey 
        /// and/or WriteValue on derived dictionary classes to control serialisation.
        /// </remarks>
        public void WriteXml(XmlWriter writer)
        {
            if (writer == null)
            {
                throw new ArgumentNullException("writer", Properties.Resources.ExValueNotNull);
            }

            foreach (var key in this.Keys)
            {
                writer.WriteStartElement("RuleSetItem", Properties.Resources.Namespace);

                this.WriteKey(writer, key);
                this.WriteValue(writer, this[key]);

                writer.WriteEndElement();
            }
        }

        /// <summary>
        /// This method is reserved and should not be used.
        /// </summary>
        /// <returns>
        /// An XmlSchema that describes the XML representation of the object that is produced 
        /// by the WriteXml method and consumed by the ReadXml method.
        /// </returns>
        public XmlSchema GetSchema()
        {
            return null;
        }
        
        /// <summary>
        /// Reads a key value as an arbitrary object type.
        /// </summary>
        /// <param name="reader">An XML reader for the serialised dictionary.</param>
        /// <returns>A key value of arbitrary type.</returns>
        protected virtual string ReadKey(XmlReader reader)
        {
            if (reader == null)
            {
                throw new ArgumentNullException("reader", Properties.Resources.ExValueNotNull);
            }

            if (reader.LocalName != "Key")
            {
                reader.ReadToFollowing("Key");
            }

            return reader.ReadElementContentAsString();
        }

        /// <summary>
        /// Reads a key value as an arbitrary object type.
        /// </summary>
        /// <param name="reader">An XML reader for the serialised dictionary.</param>
        /// <returns>A key value of arbitrary type.</returns>
        protected virtual RuleSet ReadValue(XmlReader reader)
        {
            if (reader == null)
            {
                throw new ArgumentNullException("reader", Properties.Resources.ExValueNotNull);
            }

            var serializer = new XmlSerializer(typeof(RuleSet));

            if (reader.LocalName != "Value")
            {
                reader.ReadToFollowing("Value");
            }

            using (var ms = new MemoryStream())
            {
                var sw = new StreamWriter(ms);
                sw.Write(reader.ReadInnerXml());
                sw.Flush();
                ms.Seek(0, SeekOrigin.Begin);

                return (RuleSet)serializer.Deserialize(ms);
            }
        }

        /// <summary>
        /// Writes a key value of arbitrary type to the serialisable dictionary.
        /// </summary>
        /// <param name="writer">An XML writer for the serialisable dictionary.</param>
        /// <param name="key">The key value to be serialized.</param>
        protected virtual void WriteKey(XmlWriter writer, string key)
        {
            if (writer == null)
            {
                throw new ArgumentNullException("writer", Properties.Resources.ExValueNotNull);
            }

            writer.WriteElementString("Key", Properties.Resources.Namespace, key);
        }

        /// <summary>
        /// Writes a value of arbitrary type to the serialisable dictionary.
        /// </summary>
        /// <param name="writer">An XML writer for the serialisable dictionary.</param>
        /// <param name="ruleSet">The RuleSet to be serialized.</param>
        protected virtual void WriteValue(XmlWriter writer, RuleSet ruleSet)
        {
            if (writer == null)
            {
                throw new ArgumentNullException("writer", Properties.Resources.ExValueNotNull);
            }

            writer.WriteStartElement("Value");

            using (var ms = new MemoryStream())
            {
                var serializer = new XmlSerializer(typeof(RuleSet));
                serializer.Serialize(ms, ruleSet);
                ms.Seek(0, SeekOrigin.Begin);
                var xmlReader = new XmlTextReader(ms);
                xmlReader.ReadToFollowing("RuleSet", Properties.Resources.Namespace);
                writer.WriteRaw(xmlReader.ReadOuterXml());
                writer.WriteEndElement();
            }
        }
    }
}
